from typing import Callable, Optional
import numpy as np
from datetime import datetime as datetime_
from datetime import timedelta

from core.map_const import IntervalMap
from core.object import BarData, TickData
from core.constant import Interval


# k线生成器
class BarGenerator:
    """
    For:
    1. generating 1 minute bar support from tick support
    2. generating x minute bar/x hour bar support from 1 minute support
    Notice:
    1. for x minute bar, x must be able to divide 60: 2, 3, 5, 6, 10, 15, 20, 30
    2. for x hour bar, x can be any number
    """

    def __init__(
        self,
        symbol: str,
        interval: Interval = Interval.MINUTE,
        on_window_bar: Callable = None
    ) -> None:
        """Constructor"""
        self.bar: BarData = None

        self.interval: Interval = interval
        self.interval_count: int = 0

        self.hour_bar: BarData = None

        self.symbol: str = symbol
        self.window_bar: BarData = None
        self.on_window_bar: Callable = on_window_bar

        self.last_tick: TickData = None
        self.hms_timedelta: timedelta = timedelta(hours=0, minutes=0, seconds=0)

        self.is_found = False

    def update_tick(self, tick: TickData) -> None:
        """
        Update new tick support into generator.
        """
        new_minute: bool = False

        # Filter tick support with 0 last price
        if not tick.last_price:
            return

        # Filter tick support with older timestamp
        if self.last_tick and tick.datetime < self.last_tick.datetime:
            return

        if not self.bar:
            new_minute = True
        elif (
            (self.bar.datetime.minute != tick.datetime.minute)
            or (self.bar.datetime.hour != tick.datetime.hour)
        ):
            self.bar.datetime = self.bar.datetime.replace(
                second=0, microsecond=0
            )
            self.on_bar(self.bar)

            new_minute = True

        if new_minute:
            self.bar = BarData(
                symbol=tick.symbol,
                exchange=tick.exchange,
                interval=Interval.MINUTE,
                datetime=tick.datetime,
                open_price=tick.last_price,
                high_price=tick.last_price,
                low_price=tick.last_price,
                close_price=tick.last_price,
                open_interest=tick.open_interest
            )
        else:
            self.bar.high_price = max(self.bar.high_price, tick.last_price)
            if tick.high_price > self.last_tick.high_price:
                self.bar.high_price = max(self.bar.high_price, tick.high_price)

            self.bar.low_price = min(self.bar.low_price, tick.last_price)
            if tick.low_price < self.last_tick.low_price:
                self.bar.low_price = min(self.bar.low_price, tick.low_price)

            self.bar.close_price = tick.last_price
            self.bar.open_interest = tick.open_interest
            self.bar.datetime = tick.datetime

        if self.last_tick:
            volume_change: float = tick.volume - self.last_tick.volume
            self.bar.volume += max(volume_change, 0)

            turnover_change: float = tick.turnover - self.last_tick.turnover
            self.bar.turnover += max(turnover_change, 0)

        self.last_tick = tick

    def on_bar(self, bar: BarData) -> None:
        pass

    def update_bar(self, bar: BarData) -> None:
        if self.is_found:
            pass
        elif not self.is_found and self.judge(bar):
            self.is_found = True
        elif not self.is_found and not self.judge(bar):
            raise ValueError("首次传入的K线interval类型不满足生成bg指定interval类型的条件。")
        else:
            raise ValueError("未知")

        self.update_bar_window(bar)

    def judge(self, bar: BarData):
        # 判断传入的bar是否能够生成该生成器需要的满足级别要求的K线
        bar_interval = bar.interval
        generator_interval = self.interval
        bar_timedelta = IntervalMap[bar_interval]
        generator_timedelta = IntervalMap[generator_interval]
        if bar_timedelta.total_seconds() == 0:
            return False

        # 计算两个 timedelta 的总秒数
        total_seconds_td1 = generator_timedelta.total_seconds()
        total_seconds_td2 = bar_timedelta.total_seconds()

        # 判断是否能整除
        return total_seconds_td1 % total_seconds_td2 == 0

    def update_bar_window(self, bar: BarData) -> None:
        # 传入周期与生成器周期一致，直接原生返回，可保留factor_sr数据
        if bar.interval == self.interval:
            self.on_window_bar(bar)
            self.window_bar = None
            return None

        self.hms_timedelta = timedelta(hours=bar.datetime.hour, minutes=bar.datetime.minute, seconds=bar.datetime.second)
        dt: datetime_ = bar.datetime.replace(microsecond=0)
        if not self.window_bar:
            self.window_bar = BarData(
                symbol=bar.symbol,
                exchange=bar.exchange,
                datetime=dt,
                open_price=bar.open_price,
                high_price=bar.high_price,
                low_price=bar.low_price
            )
        else:
            self.window_bar.high_price = max(
                self.window_bar.high_price,
                bar.high_price
            )
            self.window_bar.low_price = min(
                self.window_bar.low_price,
                bar.low_price
            )
        # Update close price/volume/turnover into window bar
        self.window_bar.datetime = dt
        self.window_bar.close_price = bar.close_price
        self.window_bar.volume += bar.volume
        self.window_bar.turnover += bar.turnover
        self.window_bar.open_interest = bar.open_interest

        # Check if window bar completed
        if self.hms_timedelta.total_seconds() % IntervalMap[self.interval].total_seconds() == 0:
            self.window_bar.interval = self.interval
            self.on_window_bar(self.window_bar)
            self.window_bar = None
        pass

    def generate(self) -> Optional[BarData]:
        """
        Generate the bar support and call callback immediately.
        """
        bar: BarData = self.bar

        if self.bar:
            bar.datetime = bar.datetime.replace(second=0, microsecond=0)
            self.on_bar(bar)

        self.bar = None
        return bar


# 数组管理器
class ArrayManagerBase:
    """
    For:
    1. time series container of bar support
    2. calculating technical indicator value
    """

    def __init__(self, size, symbol, interval) -> None:
        """Constructor"""
        self.count: int = 0
        self.size: int = size
        self.symbol: str = symbol
        self.interval: Interval = interval
        self.exist_factor: bool = False
        self.factor_ls: list = None
        self.inited: bool = False
        self.datetime: datetime_ = None

        self.open_array: np.ndarray = np.zeros(self.size)
        self.high_array: np.ndarray = np.zeros(self.size)
        self.low_array: np.ndarray = np.zeros(self.size)
        self.close_array: np.ndarray = np.zeros(self.size)
        self.volume_array: np.ndarray = np.zeros(self.size)
        self.turnover_array: np.ndarray = np.zeros(self.size)
        self.open_interest_array: np.ndarray = np.zeros(self.size)

    def update_bar(self, bar: BarData) -> None:
        """
        Update new bar support into array manager.
        """
        if self.datetime is None:
            # 本am第一次更新
            self.exist_factor = True if bar.factor_sr is not None else False
            if self.exist_factor:
                # 存在因子，创建相应属性
                self.factor_ls = bar.factor_sr.index.tolist()
                # 初始化因子属性
                for factor in self.factor_ls:
                    factor_existing = factor.upper()
                    setattr(self, factor_existing, np.zeros(self.size))
                    pass
            else:
                pass
            pass
        elif bar.datetime <= self.datetime:
            # bar时间小于am的最新时间，不更新
            return
        else:
            pass

        self.count += 1
        if not self.inited and self.count >= self.size:
            self.inited = True

        self.open_array[:-1] = self.open_array[1:]
        self.high_array[:-1] = self.high_array[1:]
        self.low_array[:-1] = self.low_array[1:]
        self.close_array[:-1] = self.close_array[1:]
        self.volume_array[:-1] = self.volume_array[1:]
        self.turnover_array[:-1] = self.turnover_array[1:]
        self.open_interest_array[:-1] = self.open_interest_array[1:]

        self.open_array[-1] = bar.open_price
        self.high_array[-1] = bar.high_price
        self.low_array[-1] = bar.low_price
        self.close_array[-1] = bar.close_price
        self.volume_array[-1] = bar.volume
        self.turnover_array[-1] = bar.turnover
        self.open_interest_array[-1] = bar.open_interest

        self.datetime = bar.datetime

        # 补充更新factor数组
        if self.exist_factor:
            for factor in self.factor_ls:
                factor_existing = factor.upper()
                array = getattr(self, factor_existing)
                array[:-1] = array[1:]
                array[-1] = bar.factor_sr[factor]
                setattr(self, factor_existing, array)

    @property
    def open(self) -> np.ndarray:
        """
        Get open price time series.
        """
        return self.open_array

    @property
    def high(self) -> np.ndarray:
        """
        Get high price time series.
        """
        return self.high_array

    @property
    def low(self) -> np.ndarray:
        """
        Get low price time series.
        """
        return self.low_array

    @property
    def close(self) -> np.ndarray:
        """
        Get close price time series.
        """
        return self.close_array

    @property
    def volume(self) -> np.ndarray:
        """
        Get trading volume time series.
        """
        return self.volume_array

    @property
    def turnover(self) -> np.ndarray:
        """
        Get trading turnover time series.
        """
        return self.turnover_array

    @property
    def open_interest(self) -> np.ndarray:
        """
        Get trading volume time series.
        """
        return self.open_interest_array


class BGContainer:
    def __init__(self):
        self.member_ls: list[BarGenerator] = []

    def place(self, member):
        # 根据两项参数，symbol和window查找是否已经存在成员
        symbol = member.symbol
        interval = member.interval
        is_found = False
        for member_o in self.member_ls:
            if member_o.symbol == symbol and member_o.interval == interval:
                is_found = True
                break
        if not is_found:
            self.member_ls.append(member)

    def get(self, symbol, interval):
        for member_o in self.member_ls:
            if isinstance(symbol, str):
                if member_o.symbol == symbol and member_o.interval == interval:
                    return member_o
            else:
                if member_o.symbol == symbol.value and member_o.interval == interval:
                    return member_o

        return None

    def update(self, base_bar: BarData):
        # 接收基础周期K线数据，将其在指定BG中更新
        # 若该bar对应bg未在容器中
        symbol = base_bar.symbol
        bg_ins_ls = []
        for member_o in self.member_ls:
            if member_o.symbol == symbol:
                bg_ins_ls.append(member_o)

        if len(bg_ins_ls) == 0:
            return False
        else:
            # 找到并更新
            for bg_ins in bg_ins_ls:
                bg_ins.update_bar(base_bar)
            return True
        pass


class AMContainer:
    def __init__(self):
        self.member_ls: list[ArrayManagerBase] = []
        self.members_earliest_time: datetime_ = None

    def place(self, member):
        # 根据两项参数，symbol和window查找是否已经存在成员
        symbol = member.symbol
        interval = member.interval
        is_found = False
        for member_o in self.member_ls:
            if member_o.symbol == symbol and member_o.interval == interval:
                is_found = True
                break
        if not is_found:
            self.member_ls.append(member)

    def get(self, symbol, interval):
        for member_o in self.member_ls:
            if isinstance(symbol, str):
                if member_o.symbol == symbol and member_o.interval == interval:
                    return member_o
            else:
                if member_o.symbol == symbol.value and member_o.interval == interval:
                    return member_o

        return None

    def update(self, bar: BarData):
        # 接收K线数据，将其在指定AM中更新

        pass

    def query_earliest_time(self):
        # 遍历所有am，得出最早的时间，用于帮助在新的实盘数据中尽可能少的截取数据

        pass


